use crate::ast::Name;
use fold::Folder;

// The data we will fold, a simple AST.
pub mod ast {
    pub struct Name {
        pub value: String,
    }

    pub enum Expr {
        IntLit(i64),
        Add(Box<Expr>, Box<Expr>),
        Sub(Box<Expr>, Box<Expr>),
    }

    pub enum Statement {
        Expr(Box<Expr>),
        Let(Box<Name>, Box<Expr>),
    }
}

// The abstract folder.
pub mod fold {
    use super::ast::{Expr, Name, Statement};

    pub trait Folder {
        // A leaf node just returns the node itself. In some cases, we can do this
        // to inner nodes too.
        fn fold_name(&mut self, name: Box<Name>) -> Box<Name> {
            name
        }

        // Create a new inner node by folding its children.
        #[allow(clippy::boxed_local)]
        fn fold_statement(&mut self, statement: Box<Statement>) -> Box<Statement> {
            match *statement {
                Statement::Expr(expr) => Box::new(Statement::Expr(self.fold_expr(expr))),
                Statement::Let(name, expr) => {
                    Box::new(Statement::Let(self.fold_name(name), self.fold_expr(expr)))
                }
            }
        }

        fn fold_expr(&mut self, expr: Box<Expr>) -> Box<Expr> {
            match *expr {
                Expr::IntLit(_) => expr,
                Expr::Add(lhs, rhs) => {
                    let lhs_res = self.fold_expr(lhs);
                    let rhs_res = self.fold_expr(rhs);
                    match (*lhs_res, *rhs_res) {
                        (Expr::IntLit(lhs_res), Expr::IntLit(rhs_res)) => {
                            Box::new(Expr::IntLit(lhs_res + rhs_res))
                        }
                        _ => panic!("Expected Expr::IntLit for both lhs_res and rhs_res"),
                    }
                }
                Expr::Sub(lhs, rhs) => {
                    let lhs_res = self.fold_expr(lhs);
                    let rhs_res = self.fold_expr(rhs);
                    match (*lhs_res, *rhs_res) {
                        (Expr::IntLit(lhs_res), Expr::IntLit(rhs_res)) => {
                            Box::new(Expr::IntLit(lhs_res - rhs_res))
                        }
                        _ => panic!("Expected Expr::IntLit for both lhs_res and rhs_res"),
                    }
                }
            }
        }
    }
}

#[derive(Default)]
struct FolderClient;

impl Folder for FolderClient {}

// An example concrete implementation - renames every name to 'foo'.
#[derive(Default)]
struct Renamer;

impl Folder for Renamer {
    fn fold_name(&mut self, _name: Box<Name>) -> Box<Name> {
        Box::new(Name {
            value: "foo".to_string(),
        })
    }

    // Use the default methods for the other nodes.
}

fn main() {}

#[cfg(test)]
mod folder_client_tests {
    use super::ast::{Expr, Name, Statement};
    use super::fold::Folder;
    use super::FolderClient;

    fn assert_int_expr(result: Expr, expected_value: i64) {
        match result {
            Expr::IntLit(val) => assert_eq!(expected_value, val),
            _ => panic!("Expected Expr::IntLit"),
        }
    }

    fn assert_statement_as_int_expr(result: Statement, expected_value: i64) {
        match result {
            Statement::Expr(expr) => assert_int_expr(*expr, expected_value),
            _ => panic!("Expected Statement::Expr"),
        }
    }

    fn assert_statement_as_let_name_equal_to_int_expr(
        result: Statement,
        expected_name: &str,
        expected_value: i64,
    ) {
        match result {
            Statement::Let(name, expr) => {
                assert_eq!(expected_name, name.value);
                assert_int_expr(*expr, expected_value)
            }
            _ => panic!("Expected Statement::Let"),
        }
    }

    #[test]
    fn name() {
        let mut client = FolderClient::default();
        let name = Box::new(Name {
            value: "x".to_string(),
        });

        let res = client.fold_name(name);

        assert_eq!("x".to_string(), res.value);
    }

    #[test]
    fn expr_as_int() {
        let mut client = FolderClient::default();
        let expr = Box::new(Expr::IntLit(1));

        let res = client.fold_expr(expr);

        assert_int_expr(*res, 1);
    }

    #[test]
    fn expr_as_add() {
        let mut client = FolderClient::default();
        let expr = Box::new(Expr::Add(
            Box::new(Expr::IntLit(1)),
            Box::new(Expr::IntLit(2)),
        ));

        let res = client.fold_expr(expr);

        assert_int_expr(*res, 3);
    }

    #[test]
    fn expr_as_sub() {
        let mut client = FolderClient::default();
        let expr = Box::new(Expr::Sub(
            Box::new(Expr::IntLit(1)),
            Box::new(Expr::IntLit(2)),
        ));

        let res = client.fold_expr(expr);

        assert_int_expr(*res, -1);
    }

    #[test]
    fn statement_as_let_x_equal_to_int_expr() {
        let mut client = FolderClient::default();
        let statement = Box::new(Statement::Let(
            Box::new(Name {
                value: "x".to_string(),
            }),
            Box::new(Expr::IntLit(1)),
        ));

        let res = client.fold_statement(statement);

        assert_statement_as_let_name_equal_to_int_expr(*res, "x", 1);
    }

    #[test]
    fn statement_as_let_x_equal_to_add_expr() {
        let mut client = FolderClient::default();
        let statement = Box::new(Statement::Let(
            Box::new(Name {
                value: "x".to_string(),
            }),
            Box::new(Expr::Add(
                Box::new(Expr::IntLit(1)),
                Box::new(Expr::IntLit(2)),
            )),
        ));

        let res = client.fold_statement(statement);

        assert_statement_as_let_name_equal_to_int_expr(*res, "x", 3);
    }

    #[test]
    fn statement_as_int_expr() {
        let mut client = FolderClient::default();
        let statement = Box::new(Statement::Expr(Box::new(Expr::IntLit(1))));

        let res = client.fold_statement(statement);

        assert_statement_as_int_expr(*res, 1);
    }

    #[test]
    fn statement_as_add_expr() {
        let mut client = FolderClient::default();
        let statement = Box::new(Statement::Expr(Box::new(Expr::Add(
            Box::new(Expr::IntLit(1)),
            Box::new(Expr::IntLit(2)),
        ))));

        let res = client.fold_statement(statement);

        assert_statement_as_int_expr(*res, 3);
    }

    #[test]
    fn statement_as_sub_expr() {
        let mut client = FolderClient::default();
        let statement = Box::new(Statement::Expr(Box::new(Expr::Sub(
            Box::new(Expr::IntLit(1)),
            Box::new(Expr::IntLit(2)),
        ))));

        let res = client.fold_statement(statement);

        assert_statement_as_int_expr(*res, -1);
    }
}

#[cfg(test)]
mod renamer_tests {
    use super::ast::Name;
    use super::fold::Folder;
    use super::Renamer;

    #[test]
    fn name() {
        let mut client = Renamer::default();
        let name = Box::new(Name {
            value: "x".to_string(),
        });

        let res = client.fold_name(name);

        assert_eq!("foo".to_string(), res.value);
    }
}
